/*
 *   Copyright 2012-present OSBI Ltd
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */

// Packages
import $ from 'jquery';
import _ from 'lodash';

// Renders
import { SaikuRendererOptions } from './SaikuRendererOptions';

// Utils
import {
  ROWS,
  COLUMNS,
  ROW_HEADER,
  ROW_HEADER_HEADER,
  COLUMN_HEADER
} from '../utils/constants';

class SaikuTableRenderer {
  constructor(data, options) {
    this.data = data;
    this.options = Object.assign({}, SaikuRendererOptions, options);
  }

  static genTotalDataCells(
    currentIndex,
    cellIndex,
    scanSums,
    scanIndexes,
    lists
  ) {
    const listsRows = lists[ROWS];
    let contents = '';

    for (let i = scanSums.length - 1; i >= 0; i--) {
      if (currentIndex === scanSums[i]) {
        const currentListNode = listsRows[i][scanIndexes[i]];

        for (let m = 0; m < currentListNode.cells.length; m++) {
          contents += `<td class="data total">${currentListNode.cells[m][cellIndex].value}</td>`;
        }

        scanIndexes[i]++;

        if (scanIndexes[i] < listsRows[i].length) {
          scanSums[i] += listsRows[i][scanIndexes[i]].width;
        }
      }
    }

    return contents;
  }

  static genTotalHeaderCells(
    currentIndex,
    bottom,
    scanSums,
    scanIndexes,
    lists,
    wrapContent
  ) {
    let contents = '';

    for (let i = bottom; i >= 0; i--) {
      if (currentIndex === scanSums[i]) {
        const currentListNode = lists[i][scanIndexes[i]];
        let cssClass;

        if (i === 0 && bottom === 1) {
          cssClass = 'col';
        } else if (i === bottom) {
          cssClass = 'col_total_corner';
        } else if (i === bottom - 1 && currentListNode.captions) {
          cssClass = 'col_total_first';
        } else {
          cssClass = 'col_null';
        }

        for (let m = 0; m < currentListNode.cells.length; m++) {
          let text = '&nbsp;';

          if (bottom === lists.length - 1) {
            if (currentListNode.captions) {
              text = lists[i][scanIndexes[i]].captions[m];
            }

            if (i === 0 && scanIndexes[i] === 0) {
              if (currentListNode.captions) {
                text += '&nbsp;';
              } else {
                text = '';
              }

              text += 'Grand Total';
            }
          }

          contents += `<th class="${cssClass}">${
            wrapContent ? `<div>${text}</div>` : text
          }</th>`;
        }

        scanIndexes[i]++;

        if (scanIndexes[i] < lists[i].length) {
          scanSums[i] += lists[i][scanIndexes[i]].width;
        }
      }
    }

    return contents;
  }

  static totalIntersectionCells(
    currentIndex,
    bottom,
    scanSums,
    scanIndexes,
    lists
  ) {
    let contents = '';

    for (let i = bottom; i >= 0; i--) {
      if (currentIndex === scanSums[i]) {
        const currentListNode = lists[i][scanIndexes[i]];
        const cssClass = 'data total';

        for (let m = 0; m < currentListNode.cells.length; m++) {
          const text = '&nbsp;';

          contents += `<td class="${cssClass}">${text}</td>`;
        }

        scanIndexes[i]++;

        if (scanIndexes[i] < lists[i].length) {
          scanSums[i] += lists[i][scanIndexes[i]].width;
        }
      }
    }

    return contents;
  }

  static genTotalHeaderRowCells(
    currentIndex,
    scanSums,
    scanIndexes,
    totalsLists,
    wrapContent
  ) {
    const colLists = totalsLists[COLUMNS];
    const colScanSums = scanSums[COLUMNS];
    const colScanIndexes = scanIndexes[COLUMNS];
    const bottom = colLists.length - 2;
    let contents = '';

    for (let i = bottom; i >= 0; i--) {
      if (currentIndex === colScanSums[i]) {
        for (let m = 0; m < colLists[i][colScanIndexes[i]].cells.length; m++) {
          contents += '<tr>';

          for (let j = 0; j <= bottom; j++) {
            let cssClass;
            let text = '&nbsp;';

            if (i === 0 && j === 0) {
              cssClass = 'row';
            } else if (i === j + 1) {
              cssClass = 'row_total_corner';
            } else if (i === j && colLists[i][colScanIndexes[i]].captions) {
              cssClass = 'row_total_first';
            } else if (i < j + 1) {
              cssClass = 'row_total';
            } else {
              cssClass = 'row_null';
            }

            if (j === bottom) {
              if (colLists[i][colScanIndexes[i]].captions) {
                text = colLists[i][colScanIndexes[i]].captions[m];
              }

              if (i === 0 && colScanIndexes[i] === 0) {
                if (colLists[i][colScanIndexes[i]].captions) {
                  text += '&nbsp;';
                } else {
                  text = '';
                }

                text += 'Grand Total';
              }
            }

            contents += `<th class="${cssClass}">${
              wrapContent ? `<div>${text}</div>` : text
            }</th>`;
          }

          scanIndexes = {};
          scanSums = {};

          for (let z = 0; z < totalsLists[ROWS].length; z++) {
            scanIndexes[z] = 0;
            scanSums[z] = totalsLists[ROWS][z][scanIndexes[z]].width;
          }

          for (
            let k = 0;
            k < colLists[i][colScanIndexes[i]].cells[m].length;
            k++
          ) {
            contents += `<td class="data total">${colLists[i][colScanIndexes[i]].cells[m][k].value}</td>`;

            contents += SaikuTableRenderer.totalIntersectionCells(
              k + 1,
              totalsLists[ROWS].length - 1,
              scanSums,
              scanIndexes,
              totalsLists[ROWS]
            );
          }

          contents += '</tr>';
        }

        colScanIndexes[i]++;

        if (colScanIndexes[i] < colLists[i].length) {
          colScanSums[i] += colLists[i][colScanIndexes[i]].width;
        }
      }
    }

    return contents;
  }

  static nextParentsDiffer(data, row, col) {
    while (row-- > 0) {
      if (
        data[row][col].properties.uniquename !==
        data[row][col + 1].properties.uniquename
      ) {
        return true;
      }
    }

    return false;
  }

  static topParentsDiffer(data, row, col) {
    while (col-- > 0) {
      if (
        data[row][col].properties.uniquename !==
        data[row - 1][col].properties.uniquename
      ) {
        return true;
      }
    }

    return false;
  }

  render(data, options) {
    const self = this;
    let $htmlTable;

    if (data) {
      this.data = data;
    }

    if (options) {
      this.options = Object.assign({}, SaikuRendererOptions, options);
    }

    if (typeof this.data === 'undefined') {
      return;
    }

    if (this.data !== null && this.data.error !== null) {
      return;
    }

    if (
      this.data === null ||
      (this.data.cellset && this.data.cellset.length === 0)
    ) {
      return;
    }

    if (this.options.htmlObject) {
      if (this.options.hasOwnProperty('batch')) {
        $(this.options.htmlObject)
          .parent()
          .parent()
          .unbind('scroll');
      }

      _.defer(() => {
        if (
          self.options.hasOwnProperty('batch') &&
          !self.options.hasOwnProperty('batchSize')
        ) {
          self.options['batchSize'] = 1000;
        }

        $htmlTable = self.internalRender();
        $(self.options.htmlObject).html($htmlTable);

        _.defer(() => {
          if (
            self.options.hasOwnProperty('batch') &&
            self.options.hasBatchResult
          ) {
            const batchIntervalSize = self.options.hasOwnProperty(
              'batchIntervalSize'
            )
              ? self.options.batchIntervalSize
              : 20;
            const batchIntervalTime = self.options.hasOwnProperty(
              'batchIntervalTime'
            )
              ? self.options.batchIntervalTime
              : 20;
            const batchResultLen = self.options.batchResult.length;
            let batchRow = 0;
            let batchIsRunning = false;

            const batchInsert = () => {
              if (
                !batchIsRunning &&
                batchResultLen > 0 &&
                batchRow < batchResultLen
              ) {
                const startBatchRow = batchRow;
                let batchContent = '';

                batchIsRunning = true;

                for (
                  let i = 0;
                  batchRow < batchResultLen && i < batchIntervalSize;
                  i++, batchRow++
                ) {
                  batchContent += self.options.batchResult[batchRow];
                }

                if (batchRow > startBatchRow) {
                  $(self.options.htmlObject).append($(batchContent));
                }

                batchIsRunning = false;
              }

              if (batchRow >= batchResultLen) {
                $(self.options.htmlObject)
                  .parent()
                  .parent()
                  .unbind('scroll');
              }
            };

            const lazyBatchInsert = _.debounce(batchInsert, batchIntervalTime);

            $(self.options.htmlObject)
              .parent()
              .parent()
              .scroll(() => {
                lazyBatchInsert();
              });
          }
        });

        return $htmlTable;
      });
    } else {
      $htmlTable = this.internalRender();

      return $htmlTable;
    }
  }

  clear() {
    if (
      this.options &&
      this.options.htmlObject &&
      this.options.hasOwnProperty('batch')
    ) {
      $(this.options.htmlObject)
        .parent()
        .parent()
        .unbind('scroll');
    }
  }

  processData() {
    this.hasProcessed = true;
  }

  internalRender() {
    const data = this.data.cellset;
    const table = data ? data : [];
    const dirs = [ROWS, COLUMNS];
    const rowGroups = [];
    const resultRows = [];
    const totalsLists = {};
    const scanSums = {};
    const scanIndexes = {};
    let tableContent = '';
    let rowContent = '';
    // let processedRowHeader = false;
    let lowestRowLvl = 0;
    let batchSize = null;
    let batchStarted = false;
    let isColHeader = false;
    let isColHeaderDone = false;
    let wrapContent = true;
    let colSpan;
    // let colValue;
    let isHeaderLowestLvl;
    let firstColumn;
    let isLastColumn;
    // let isLastRow;
    let nextHeader;
    let group;

    if (this.options) {
      batchSize = this.options.hasOwnProperty('batchSize')
        ? this.options.batchSize
        : null;

      wrapContent = this.options.hasOwnProperty('wrapContent')
        ? this.options.wrapContent
        : true;
    }

    totalsLists[COLUMNS] = this.data.rowTotalsLists;
    totalsLists[ROWS] = this.data.colTotalsLists;

    for (let i = 0; i < dirs.length; i++) {
      scanSums[dirs[i]] = [];
      scanIndexes[dirs[i]] = [];
    }

    if (totalsLists[COLUMNS]) {
      for (let i = 0; i < totalsLists[COLUMNS].length; i++) {
        scanIndexes[COLUMNS][i] = 0;
        scanSums[COLUMNS][i] =
          totalsLists[COLUMNS][i][scanIndexes[COLUMNS][i]].width;
      }
    }

    for (let row = 0, rowLen = table.length; row < rowLen; row++) {
      const rowShifted = row - this.data.topOffset;
      let headerSame = false;

      colSpan = 1;
      // colValue = '';
      isHeaderLowestLvl = false;
      isLastColumn = false;
      // isLastRow = false;
      isColHeader = false;

      if (totalsLists[ROWS]) {
        for (let i = 0; i < totalsLists[ROWS].length; i++) {
          scanIndexes[ROWS][i] = 0;
          scanSums[ROWS][i] = totalsLists[ROWS][i][scanIndexes[ROWS][i]].width;
        }
      }

      rowContent = '<tr>';

      if (row === 0) {
        rowContent = `<thead>${rowContent}`;
      }

      for (let col = 0, colLen = table[row].length; col < colLen; col++) {
        const colShifted = col - this.data.leftOffset;
        const header = data[row][col];

        if (header.type === COLUMN_HEADER) {
          isColHeader = true;
        }

        // If the cell is a column header and is null (top left of table)
        if (
          header.type === COLUMN_HEADER &&
          header.value === 'null' &&
          (firstColumn === null || col < firstColumn)
        ) {
          rowContent += '<th class="all_null">&nbsp;</th>';
        } // If the cell is a column header and isn't null (column header of table)
        else if (header.type === COLUMN_HEADER) {
          if (firstColumn === null) {
            firstColumn = col;
          }

          if (table[row].length === col + 1) {
            isLastColumn = true;
          } else {
            nextHeader = data[row][col + 1];
          }

          if (isLastColumn) {
            // Last column in a row...
            if (header.value === 'null') {
              rowContent += '<th class="col_null">&nbsp;</th>';
            } else {
              if (totalsLists[ROWS]) {
                colSpan =
                  totalsLists[ROWS][row + 1][scanIndexes[ROWS][row + 1]].span;
              }

              rowContent += `<th class="col" style="text-align: center;" colspan="${colSpan}" title="${
                header.value
              }">${
                wrapContent
                  ? `<div rel="${row}:${col}">${header.value}</div>`
                  : header.value
              }</th>`;
            }
          } else {
            // All the rest...
            const groupChange =
              col > 1 && row > 1 && !isHeaderLowestLvl && col > firstColumn
                ? data[row - 1][col + 1].value !== data[row - 1][col].value ||
                  data[row - 1][col + 1].properties.uniquename !==
                    data[row - 1][col].properties.uniquename
                : false;
            const maxColspan = colSpan > 999 ? true : false;

            if (
              header.value !== nextHeader.value ||
              SaikuTableRenderer.nextParentsDiffer(data, row, col) ||
              isHeaderLowestLvl ||
              groupChange ||
              maxColspan
            ) {
              if (header.value === 'null') {
                rowContent += `<th class="col_null" colspan="${colSpan}">&nbsp;</th>`;
              } else {
                if (totalsLists[ROWS]) {
                  colSpan =
                    totalsLists[ROWS][row + 1][scanIndexes[ROWS][row + 1]].span;
                }

                rowContent += `<th class="col" style="text-align: center;" colspan="${
                  colSpan === 0 ? 1 : colSpan
                }" title="${header.value}">${
                  wrapContent
                    ? `<div rel="${row}:${col}">${header.value}</div>`
                    : header.value
                }</th>`;
              }

              colSpan = 1;
            } else {
              colSpan++;
            }
          }

          if (totalsLists[ROWS]) {
            rowContent += SaikuTableRenderer.genTotalHeaderCells(
              col - this.data.leftOffset + 1,
              row + 1,
              scanSums[ROWS],
              scanIndexes[ROWS],
              totalsLists[ROWS],
              wrapContent
            );
          }
        } // If the cell is a row header and is null (grouped row header)
        else if (header.type === ROW_HEADER && header.value === 'null') {
          rowContent += '<th class="row_null">&nbsp;</th>';
        } // If the cell is a row header and isn't null (last row header)
        else if (header.type === ROW_HEADER) {
          if (lowestRowLvl === col) {
            isHeaderLowestLvl = true;
          } else {
            nextHeader = data[row][col + 1];
          }

          const previousRow = data[row - 1];
          const same =
            !headerSame &&
            !isHeaderLowestLvl &&
            (col === 0 ||
              !SaikuTableRenderer.topParentsDiffer(data, row, col)) &&
            header.value === previousRow[col].value;

          headerSame = !same;

          let value = same
            ? '<div>&nbsp;</div>'
            : `<div rel="${row}:${col}">${header.value}</div>`;

          if (!wrapContent) {
            value = same ? '&nbsp;' : header.value;
          }

          const tipsy = '';
          const cssclass = same ? 'row_null' : 'row';
          let colspan = 0;

          if (
            !isHeaderLowestLvl &&
            (typeof nextHeader === 'undefined' || nextHeader.value === 'null')
          ) {
            group = header.properties.dimension;
            const level = header.properties.level;
            const groupWidth =
              group in rowGroups
                ? rowGroups[group].length - rowGroups[group].indexOf(level)
                : 1;

            colspan = 1;

            for (
              let k = col + 1;
              colspan < groupWidth &&
              k <= lowestRowLvl + 1 &&
              data[row][k] !== 'null';
              k++
            ) {
              colspan = k - col;
            }

            col = col + colspan - 1;
          }

          rowContent += `<th class="${cssclass}" ${
            colspan > 0 ? ` colspan="${colspan}"` : ''
          }${tipsy}>${value}</th>`;
        } else if (header.type === ROW_HEADER_HEADER) {
          rowContent += `<th class="row_header">${
            wrapContent ? `<div>${header.value}</div>` : header.value
          }</th>`;
          isHeaderLowestLvl = true;
          // processedRowHeader = true;
          lowestRowLvl = col;

          if (header.properties.hasOwnProperty('dimension')) {
            group = header.properties.dimension;

            if (!(group in rowGroups)) {
              rowGroups[group] = [];
            }

            rowGroups[group].push(header.properties.level);
          }
        } // If the cell is a normal data cell
        else if (header.type === 'DATA_CELL') {
          let color = '';
          let val = header.value;
          let arrow = '';

          batchStarted = true;

          if (header.properties.hasOwnProperty('image')) {
            const img_height = header.properties.hasOwnProperty('image_height')
              ? ` height="${header.properties.image_height}"`
              : '';
            const img_width = header.properties.hasOwnProperty('image_width')
              ? ` width="${header.properties.image_width}"`
              : '';
            val = `<img ${img_height} ${img_width} style="padding-left: 5px" src="${header.properties.image}" border="0">`;
          }

          if (header.properties.hasOwnProperty('style')) {
            color = ` style="background-color: ${header.properties.style}" `;
          }

          if (header.properties.hasOwnProperty('link')) {
            val = `<a target="_blank" href="${header.properties.link}">${val}</a>`;
          }

          if (header.properties.hasOwnProperty('arrow')) {
            arrow = `<img height="10" width="10" style="padding-left: 5px" src="./images/arrow-${header.properties.arrow}.gif" border="0">`;
          }

          rowContent += `<td class="data" ${color}>${
            wrapContent
              ? `<div class="datadiv" alt="${header.properties.raw}" rel="${header.properties.position}">`
              : ''
          }${val}${arrow}${wrapContent ? '</div>' : ''}</td>`;

          if (totalsLists[ROWS]) {
            rowContent += SaikuTableRenderer.genTotalDataCells(
              colShifted + 1,
              rowShifted,
              scanSums[ROWS],
              scanIndexes[ROWS],
              totalsLists,
              wrapContent
            );
          }
        }
      }

      let totals = '';

      rowContent += '</tr>';

      if (totalsLists[COLUMNS] && rowShifted >= 0) {
        totals += SaikuTableRenderer.genTotalHeaderRowCells(
          rowShifted + 1,
          scanSums,
          scanIndexes,
          totalsLists,
          wrapContent
        );
      }

      if (batchStarted && batchSize) {
        if (row <= batchSize) {
          if (!isColHeader && !isColHeaderDone) {
            tableContent += '</thead><tbody>';
            isColHeaderDone = true;
          }

          tableContent += rowContent;

          if (totals.length > 0) {
            tableContent += totals;
          }
        } else {
          resultRows.push(rowContent);

          if (totals.length > 0) {
            resultRows.push(totals);
          }
        }
      } else {
        if (!isColHeader && !isColHeaderDone) {
          tableContent += '</thead><tbody>';
          isColHeaderDone = true;
        }

        tableContent += rowContent;

        if (totals.length > 0) {
          tableContent += totals;
        }
      }
    }

    if (this.options) {
      this.options['batchResult'] = resultRows;
      this.options['hasBatchResult'] = resultRows.length > 0;
    }

    return `<table>${tableContent}</tbody></table>`;
  }
}

export default SaikuTableRenderer;
